﻿using Levfier.Proccessors.Parameters;
using System;

namespace Levfier.Proccessors.NoiseReduction
{
    /// <summary>
    /// スペクトラルサブトラクション法によるノイズサプレッサ
    /// </summary>
    [Serializable]
    public class SpectralSubtraction : Proccessor
    {
        /// <summary>
        /// 減算量に関するパラメータ
        /// </summary>
        private ProccessParameter effectParam;

        /// <summary>
        /// Binの精度[Hz]
        /// </summary>
        private ProccessParameter accuracyParam;

        /// <summary>
        /// サンプルカウント
        /// </summary>
        private int sampleCount;

        /// <summary>
        /// FFTフレームのサイズ
        /// 2の冪乗
        /// </summary>
        private int frameSize;

        /// <summary>
        /// フレームサイズの2の対数
        /// </summary>
        private int bitSize;

        /// <summary>
        /// 入力の生データ
        /// </summary>
        private double[] rawData;

        /// <summary>
        /// 入力信号
        /// </summary>
        private double[] inputReal;

        /// <summary>
        /// 入力信号の複素数領域
        /// ダミーとして使う
        /// </summary>
        private double[] inputImage;

        /// <summary>
        /// FFT結果
        /// </summary>
        private double[] outputReal;

        /// <summary>
        /// FFT結果の複素数領域
        /// </summary>
        private double[] outputImage;

        /// <summary>
        /// ノイズリダクション後の信号
        /// </summary>
        private double[][] reulstReal;

        /// <summary>
        /// ノイズリダクション後の信号の複素数領域
        /// </summary>
        private double[][] resultImage;

        /// <summary>
        /// 前回のノイズリダクション後の信号の後半部分
        /// </summary>
        private double[] preResult;

        /// <summary>
        /// 窓関数の係数
        /// フレームサイズ分でなく、サンプルカウント分を確保
        /// </summary>
        private double[] window;

        /// <summary>
        /// ロック用のオブジェクト
        /// </summary>
        private object lockObj = new object();

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SpectralSubtraction() : base()
        {
            //サンプルカウントが変化した時のイベントハンドラを登録
            VSTParameterManager.GetInstance().SampleCountChanged += SpectralSubtraction_SampleCountChanged;
            VSTParameterManager.GetInstance().SampleRateChanged += SpectralSubtraction_SampleCountChanged;
        }

        /// <summary>
        /// 音を処理する
        /// </summary>
        /// <param name="inputBuffer">入力</param>
        /// <param name="outputBuffer">出力</param>
        protected override void InnerProccess(double[] inputBuffer, double[] outputBuffer)
        {
            lock (lockObj)
            {
                int sampleCountHalf = this.sampleCount / 2;
                double effectTemp = ProccessParameter.ConvertDBToValue(this.effectParam.Value);
                double effect = effectTemp * effectTemp;
                double power = 0.0d;
                double powerRate = 0.0d;

                //計算 オーバラップ分繰り返す
                for (int i = 0; i < 2; i++)
                {
                    //音を前半にずらすて新しい音を後半に格納
                    this.PushInputFrameHalf(inputBuffer, i * sampleCountHalf);

                    //生データに窓関数を適用し、格納
                    this.ApplyWindow(this.rawData, this.inputReal);

                    //フーリエ変換
                    CommonMath.FFT(this.inputReal, this.inputImage, out this.outputReal, out this.outputImage, this.bitSize);

                    for (int j = 0; j < this.frameSize; j++)
                    {
                        //パワースペクトルを算出
                        power = this.outputReal[j] * this.outputReal[j] + this.outputImage[j] * this.outputImage[j];

                        //スレッショルドを下回った場合、スペクトル減算する
                        if (effect >= power)
                        {
                            //powerRate = 0.0d;
                            this.outputReal[j] = 0.0d;
                            this.outputImage[j] = 0.0d;
                        }
                        else
                        {
                            //スレッショルド以上の場合、そのまま出力
                            powerRate = (power - effect) / power;

                            this.outputReal[j] *= powerRate;
                            this.outputImage[j] *= powerRate;
                        }
                    }

                    CommonMath.IFFT(this.outputReal, this.outputImage, out this.reulstReal[i], out this.resultImage[i], this.bitSize);
                }

                //前半を出力
                for (int i = 0; i < sampleCountHalf; i++)
                {
                    outputBuffer[i] = this.reulstReal[0][i] + this.preResult[i];
                }

                //後半を出力
                for (int i = sampleCountHalf; i < this.sampleCount; i++)
                {
                    outputBuffer[i] = this.reulstReal[0][i] + this.reulstReal[1][i - sampleCountHalf];
                }

                //次回の計算用に、計算結果の後半をコピーしておく
                for (int i = 0; i < sampleCountHalf; i++)
                {
                    this.preResult[i] = this.reulstReal[1][i + sampleCountHalf];
                }
            }
        }

        /// <summary>
        /// パラメータの初期化
        /// </summary>
        protected override void InitParameters()
        {
            this.effectParam = new ProccessParameter();
            this.effectParam.Name = "Effect";
            this.effectParam.Value = -100.0d;
            this.effectParam.MaxValue = 10.0d;
            this.effectParam.MinValue = -100.0d;
            this.effectParam.Step = 1.0d;
            this.parameters.Add(this.effectParam.Name, this.effectParam);

            this.accuracyParam = new ProccessParameter();
            this.accuracyParam.Name = "Accuracy[Hz]";
            this.accuracyParam.Value = 500.0d;
            this.accuracyParam.MaxValue = 1000.0d;
            this.accuracyParam.MinValue = 50.0d;
            this.accuracyParam.Step = 50.0d;
            this.accuracyParam.ValueChanged += accuracyParam_ValueChanged;
            this.parameters.Add(this.accuracyParam.Name, this.accuracyParam);

            this.InitFFT();
        }

        /// <summary>
        /// ビン精度周波数が変更された時の動作
        /// </summary>
        /// <param name="sender"></param>
        private void accuracyParam_ValueChanged(object sender)
        {
            this.InitFFT();
        }

        /// <summary>
        /// FFTの初期化
        /// </summary>
        private void InitFFT()
        {
            lock (lockObj)
            {
                //周波数精度
                double accuracyFreq = this.accuracyParam.Value;

                double sampleRate = VSTParameterManager.GetInstance().SampleRate;
                this.sampleCount = VSTParameterManager.GetInstance().SampleCount;
                this.frameSize = 1;

                //2の冪乗の最低フレームサイズを求める
                for (int i = 0; this.frameSize < sampleCount; i++)
                {
                    this.frameSize = (int)Math.Pow(2, i);
                    this.bitSize = i;
                }

                //設定された周波数精度を満たすフレームサイズを求める
                for (int i = this.bitSize; (sampleRate / (double)this.frameSize) > accuracyFreq; i++)
                {
                    this.frameSize = (int)Math.Pow(2, i);
                    this.bitSize = i;
                }

                this.rawData = new double[this.sampleCount];

                this.inputReal = new double[this.frameSize];
                this.inputImage = new double[this.frameSize];
                //this.outputReal = new double[this.frameSize];
                //this.outputImage = new double[this.frameSize];

                this.reulstReal = new double[2][];
                this.resultImage = new double[2][];

                //窓関数は実データ部分のみ
                this.window = new double[this.sampleCount];

                this.preResult = new double[this.sampleCount / 2];

                //窓係数を初期化
                this.InitWindow();
            }
        }

        /// <summary>
        /// サンプルカウントが変化した時の動作
        /// </summary>
        /// <param name="value">変更後の値</param>
        private void SpectralSubtraction_SampleCountChanged(double value)
        {
            this.InitFFT();
        }

        /// <summary>
        /// 音をフレームに格納する
        /// </summary>
        /// <param name="source">ソース</param>
        /// <param name="startIndex">開始インデックス</param>
        private void PushInputFrameHalf(double[] source, int startIndex)
        {
            int sampleCountHalf = this.sampleCount / 2;

            
            for (int i = 0; i < sampleCountHalf; i++)
            {
                ////フレームの後半を前半に移動する
                this.rawData[i] = this.rawData[i + sampleCountHalf];

                ////移動して空になった場所に新しく詰め込む
                this.rawData[i + sampleCountHalf] = source[startIndex + i];
            }
        }

        /// <summary>
        /// 窓係数を初期化する
        /// </summary>
        private void InitWindow()
        {
            //窓関数を初期化する
            for (int i = 0; i < this.sampleCount; i++)
            {
                this.window[i] = CommonMath.GetHanningWindowValue(i, this.sampleCount);
            }
        }

        /// <summary>
        /// 窓関数を適用する
        /// </summary>
        private void ApplyWindow(double[] input, double[] output)
        {
            //実データのみに窓関数を適用する
            for (int i = 0; i < this.sampleCount; i++)
            {
                output[i] = this.window[i] * input[i];
            }
        }
    }
}
